#pragma once

#include "ops.h"
#include "tuple.h"

namespace sfc::variant {

namespace detail {

template <class X, class... T>
struct TagHelper {};

template <class X, class... T>
struct TagHelper<X, X, T...> {
  static constexpr usize VALUE = 0U;
};

template <class X, class U, class... T>
struct TagHelper<X, U, T...> {
  static constexpr usize VALUE = TagHelper<X, T...>::VALUE + 1;
};

}  // namespace detail

template <class X, class... T>
using tag_t = ops::idx_t<detail::TagHelper<X, T...>::VALUE>;

#if __has_builtin(__type_pack_element)
template <usize I, typename... T>
using element_t = __type_pack_element<I, T...>;
#else
template <usize I, typename... T>
using element_t = typename detail::ElementHelper<I, T...>::Type;
#endif

namespace detail {

template <class... T>
union Raw;

// clang-format off
#define _X_ Raw(){} ~Raw(){}

template <>
union Raw<>
{};

template <class T0>
union Raw<T0>
{ _X_; T0 _0; };

template <class T0, class T1>
union Raw<T0, T1>
{ _X_; T0 _0; T1 _1; };

template <class T0, class T1, class T2>
union Raw<T0, T1, T2>
{ _X_; T0 _0; T1 _1; T2 _2; };

template <class T0, class T1, class T2, class T3>
union Raw<T0, T1, T2, T3>
{ _X_; T0 _0; T1 _1; T2 _2; T3 _3; };

template <class T0, class T1, class T2, class T3, class T4>
union Raw<T0, T1, T2, T3, T4>
{ _X_; T0 _0; T1 _1; T2 _2; T3 _3; T4 _4; };

template <class T0, class T1, class T2, class T3, class T4, class T5>
union Raw<T0, T1, T2, T3, T4, T5>
{ _X_; T0 _0; T1 _1; T2 _2; T3 _3; T4 _4; T5 _5; };

template <class T0, class T1, class T2, class T3, class T4, class T5, class T6>
union Raw<T0, T1, T2, T3, T4, T5, T6>
{ _X_; T0 _0; T1 _1; T2 _2; T3 _3; T4 _4; T5 _5; T6 _6; };

template <class T0, class T1, class T2, class T3, class T4, class T5, class T6, class T7>
union Raw<T0, T1, T2, T3, T4, T5, T6, T7>
{ _X_; T0 _0; T1 _1; T2 _2; T3 _3; T4 _4; T5 _5; T6 _6; T7 _7; };

template <class T0, class T1, class T2, class T3, class T4, class T5, class T6, class T7, class T8>
union Raw<T0, T1, T2, T3, T4, T5, T6, T7, T8>
{ _X_; T0 _0; T1 _1; T2 _2; T3 _3; T4 _4; T5 _5; T6 _6; T7 _7; T8 _8; };

template <class T0, class T1, class T2, class T3, class T4, class T5, class T6, class T7, class T8, class T9>
union Raw<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9>
{ _X_; T0 _0; T1 _1; T2 _2; T3 _3; T4 _4; T5 _5; T6 _6; T7 _7; T8 _8; T9 _9; };

#undef _X_
// clang-format on

template <class... T>
struct Inn {
  static constexpr usize SIZE = sizeof...(T);
  static_assert(SIZE <= 10);

  u8 _tag;
  Raw<T...> _raw;

 public:
  template <usize I, class... X>
  Inn(ops::idx_t<I>, X&&... x) : _tag{static_cast<u8>(I)} {
    this->ctor<I>(static_cast<X&&>(x)...);
  }

  ~Inn() {
    this->dtor();
  }

  Inn(const Inn& other) {
    this->copy_ctor(other);
  }

  Inn(Inn&& other) noexcept {
    this->move_ctor(other);
  }

  Inn& operator=(const Inn& other) {
    if (_tag == other._tag) {
      this->copy_assign(other);
    } else {
      this->dtor();
      this->copy_ctor(other);
    }
    return *this;
  }

  Inn& operator=(Inn&& other) noexcept {
    if (_tag == other._tag) {
      this->move_assign(other);
    } else {
      this->dtor();
      this->move_ctor(other);
    }
    return *this;
  }

  template <usize I>
  auto operator[](ops::idx_t<I>) const -> auto& {
    if constexpr (I == 0) return _raw._0;
    if constexpr (I == 1) return _raw._1;
    if constexpr (I == 2) return _raw._2;
    if constexpr (I == 3) return _raw._3;
    if constexpr (I == 4) return _raw._4;
    if constexpr (I == 5) return _raw._5;
    if constexpr (I == 6) return _raw._6;
    if constexpr (I == 7) return _raw._7;
    if constexpr (I == 8) return _raw._8;
    if constexpr (I == 9) return _raw._9;
  }

  template <usize I>
  auto operator[](ops::idx_t<I>) -> auto& {
    if constexpr (I == 0) return _raw._0;
    if constexpr (I == 1) return _raw._1;
    if constexpr (I == 2) return _raw._2;
    if constexpr (I == 3) return _raw._3;
    if constexpr (I == 4) return _raw._4;
    if constexpr (I == 5) return _raw._5;
    if constexpr (I == 6) return _raw._6;
    if constexpr (I == 7) return _raw._7;
    if constexpr (I == 8) return _raw._8;
    if constexpr (I == 9) return _raw._9;
  }

  auto map(auto&& f) const {
    switch (_tag) {  // clang-format off
      case 0: if constexpr(SIZE>0) return f(_raw._0);
      case 1: if constexpr(SIZE>1) return f(_raw._1);
      case 2: if constexpr(SIZE>2) return f(_raw._2);
      case 3: if constexpr(SIZE>3) return f(_raw._3);
      case 4: if constexpr(SIZE>4) return f(_raw._4);
      case 5: if constexpr(SIZE>5) return f(_raw._5);
      case 6: if constexpr(SIZE>6) return f(_raw._6);
      case 7: if constexpr(SIZE>7) return f(_raw._7);
      case 8: if constexpr(SIZE>8) return f(_raw._8);
      case 9: if constexpr(SIZE>9) return f(_raw._9);
    }  // clang-format on

    if constexpr (SIZE > 0) {
      return f(_raw._0);
    }
  }

  auto map_mut(auto&& f) {
    switch (_tag) {  // clang-format off
      case 0: if constexpr(SIZE>0) return f(_raw._0);
      case 1: if constexpr(SIZE>1) return f(_raw._1);
      case 2: if constexpr(SIZE>2) return f(_raw._2);
      case 3: if constexpr(SIZE>3) return f(_raw._3);
      case 4: if constexpr(SIZE>4) return f(_raw._4);
      case 5: if constexpr(SIZE>5) return f(_raw._5);
      case 6: if constexpr(SIZE>6) return f(_raw._6);
      case 7: if constexpr(SIZE>7) return f(_raw._7);
      case 8: if constexpr(SIZE>8) return f(_raw._8);
      case 9: if constexpr(SIZE>9) return f(_raw._9);
    }  // clang-format on

    if constexpr (SIZE > 0) {
      return f(_raw._0);
    }
  }

 private:
  template <usize I, class... X>
  void ctor(X&&... x) {
    _tag = I;

    if constexpr (I == 0) ptr::write(&_raw._0, static_cast<X&&>(x)...);
    if constexpr (I == 1) ptr::write(&_raw._1, static_cast<X&&>(x)...);
    if constexpr (I == 2) ptr::write(&_raw._2, static_cast<X&&>(x)...);
    if constexpr (I == 3) ptr::write(&_raw._3, static_cast<X&&>(x)...);
    if constexpr (I == 4) ptr::write(&_raw._4, static_cast<X&&>(x)...);
    if constexpr (I == 5) ptr::write(&_raw._5, static_cast<X&&>(x)...);
    if constexpr (I == 6) ptr::write(&_raw._6, static_cast<X&&>(x)...);
    if constexpr (I == 7) ptr::write(&_raw._7, static_cast<X&&>(x)...);
    if constexpr (I == 8) ptr::write(&_raw._8, static_cast<X&&>(x)...);
    if constexpr (I == 9) ptr::write(&_raw._9, static_cast<X&&>(x)...);
  }

  void dtor() {
    switch (_tag) {  // clang-format off
      case 0: if constexpr(SIZE>0) mem::drop(_raw._0); break;
      case 1: if constexpr(SIZE>1) mem::drop(_raw._1); break;
      case 2: if constexpr(SIZE>2) mem::drop(_raw._2); break;
      case 3: if constexpr(SIZE>3) mem::drop(_raw._3); break;
      case 4: if constexpr(SIZE>4) mem::drop(_raw._4); break;
      case 5: if constexpr(SIZE>5) mem::drop(_raw._5); break;
      case 6: if constexpr(SIZE>6) mem::drop(_raw._6); break;
      case 7: if constexpr(SIZE>7) mem::drop(_raw._7); break;
      case 8: if constexpr(SIZE>8) mem::drop(_raw._8); break;
      case 9: if constexpr(SIZE>9) mem::drop(_raw._9); break;
    }  // clang-format on
  }

  void copy_ctor(const Inn& other) {
    _tag = other._tag;
    switch (_tag) {  // clang-format off
      case 0: if constexpr(SIZE>0) ptr::write(&_raw._0, other._raw._0); break;
      case 1: if constexpr(SIZE>1) ptr::write(&_raw._1, other._raw._1); break;
      case 2: if constexpr(SIZE>2) ptr::write(&_raw._2, other._raw._2); break;
      case 3: if constexpr(SIZE>3) ptr::write(&_raw._3, other._raw._3); break;
      case 4: if constexpr(SIZE>4) ptr::write(&_raw._4, other._raw._4); break;
      case 5: if constexpr(SIZE>5) ptr::write(&_raw._5, other._raw._5); break;
      case 6: if constexpr(SIZE>6) ptr::write(&_raw._6, other._raw._6); break;
      case 7: if constexpr(SIZE>7) ptr::write(&_raw._7, other._raw._7); break;
      case 8: if constexpr(SIZE>8) ptr::write(&_raw._8, other._raw._8); break;
      case 9: if constexpr(SIZE>9) ptr::write(&_raw._9, other._raw._9); break;
    }  // clang-format on
  }

  void move_ctor(Inn& other) noexcept {
    _tag = other._tag;
    switch (_tag) {  // clang-format off
      case 0: if constexpr(SIZE>0) ptr::write(&_raw._0, mem::move(other._raw._0)); break;
      case 1: if constexpr(SIZE>1) ptr::write(&_raw._1, mem::move(other._raw._1)); break;
      case 2: if constexpr(SIZE>2) ptr::write(&_raw._2, mem::move(other._raw._2)); break;
      case 3: if constexpr(SIZE>3) ptr::write(&_raw._3, mem::move(other._raw._3)); break;
      case 4: if constexpr(SIZE>4) ptr::write(&_raw._4, mem::move(other._raw._4)); break;
      case 5: if constexpr(SIZE>5) ptr::write(&_raw._5, mem::move(other._raw._5)); break;
      case 6: if constexpr(SIZE>6) ptr::write(&_raw._6, mem::move(other._raw._6)); break;
      case 7: if constexpr(SIZE>7) ptr::write(&_raw._7, mem::move(other._raw._7)); break;
      case 8: if constexpr(SIZE>8) ptr::write(&_raw._8, mem::move(other._raw._8)); break;
      case 9: if constexpr(SIZE>9) ptr::write(&_raw._9, mem::move(other._raw._9)); break;
    }  // clang-format on
  }

  void copy_assign(const Inn& other) {
    switch (_tag) {  // clang-format off
      case 0: _raw._0 = other._raw._0; break;
      case 1: _raw._1 = other._raw._1; break;
      case 2: _raw._2 = other._raw._2; break;
      case 3: _raw._3 = other._raw._3; break;
      case 4: _raw._4 = other._raw._4; break;
      case 5: _raw._5 = other._raw._5; break;
      case 6: _raw._6 = other._raw._6; break;
      case 7: _raw._7 = other._raw._7; break;
      case 8: _raw._8 = other._raw._8; break;
      case 9: _raw._9 = other._raw._9; break;
    }  // clang-format on
  }

  void move_assign(Inn& other) {
    switch (_tag) {  // clang-format off
      case 0: if constexpr(SIZE>0) _raw._0 = mem::move(other._raw._0); break;
      case 1: if constexpr(SIZE>1) _raw._1 = mem::move(other._raw._1); break;
      case 2: if constexpr(SIZE>2) _raw._2 = mem::move(other._raw._2); break;
      case 3: if constexpr(SIZE>3) _raw._3 = mem::move(other._raw._3); break;
      case 4: if constexpr(SIZE>4) _raw._4 = mem::move(other._raw._4); break;
      case 5: if constexpr(SIZE>5) _raw._5 = mem::move(other._raw._5); break;
      case 6: if constexpr(SIZE>6) _raw._6 = mem::move(other._raw._6); break;
      case 7: if constexpr(SIZE>7) _raw._7 = mem::move(other._raw._7); break;
      case 8: if constexpr(SIZE>8) _raw._8 = mem::move(other._raw._8); break;
      case 9: if constexpr(SIZE>9) _raw._9 = mem::move(other._raw._9); break;
    }  // clang-format on
  }
};

}  // namespace detail

template <class... T>
class Variant {
  detail::Inn<T...> _inn;

 public:
  template <class X>
  Variant(X val) noexcept : _inn{tag_t<X, T...>{}, static_cast<X&&>(val)} {}

  ~Variant() = default;

  Variant(const Variant&) = default;

  Variant(Variant&&) noexcept = default;

  Variant& operator=(const Variant&) = default;

  Variant& operator=(Variant&&) noexcept = default;

  template <class X>
  auto is() const -> bool {
    static constexpr auto I = tag_t<X, T...>::VALUE;
    return _inn._tag == I;
  }

  template <class X>
  auto as() const -> option::Option<const X&> {
    static constexpr auto I = tag_t<X, T...>::VALUE;
    if (_inn._tag != I) {
      return {};
    }
    return _inn[ops::idx_t<I>{}];
  }

  template <class X>
  auto as_mut() -> option::Option<X&> {
    static constexpr auto I = tag_t<X, T...>::VALUE;
    if (_inn._tag != I) {
      return {};
    }
    return _inn[ops::idx_t<I>{}];
  }

  auto map(auto&& f) const {
    return _inn.map(f);
  }

  auto map_mut(auto&& f) {
    return _inn.map_mut(f);
  }
};

}  // namespace sfc::variant
